﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.Windows.Forms; 

using AForge.Video;
using AForge.Video.DirectShow;

using Microsoft.DirectX;

using Graphene.Processing;
using Graphene.Storing;

namespace Graphene
{    
    public class Controller
    {
        private static readonly int SLEEPTIME = 1000;
        private static readonly int MEASURING_INTERVAL = 2000;

        private static readonly string PLAYPAUSE_BUTTON_TEXT_START = "&Start";
        private static readonly string PLAYPAUSE_BUTTON_TEXT_STOP = "&Stop";

        public Controller(Model mo, MainForm vi)
        {
            this.model = mo;
            this.view = vi;
            this.markerCalibrateController = new MarkerCalibrateController(this.model, this.view);
            this._3dProcessingController = new StereoProcessingController(this.model, this.view);           

            this.perSecondStopWatch = new Stopwatch();
            this.perSecondStopWatch.Start();

            this.model.MarkersList_AddChangedHandler(this.view.UpdateGridForMarkerCalibration);
            this.model.MarkersList_AddChangedHandler(this.view.UpdateGridForStereoProcessing);

            this.view.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.MainFormClosed);
            this.view.ExitToolStripMenuItem.Click += this.ExitToolStripMenuItem_Click;
            this.view.PlayPauseVideoButton.Click += new System.EventHandler(this.PlayPauseVideoButton_Click);
            this.view.PlayPauseProcessorButton.Click += new System.EventHandler(this.PlayPauseProcessorButton_Click);
            this.view.VideoDeviceDropdown.SelectionChangeCommitted += new System.EventHandler(this.VideoDeviceDropdown_NewSelection);
            this.view.MouselookCheckbox.CheckedChanged += new System.EventHandler(this.MouselookCheckbox_OnCheckChange);
            this.view.DirectXPanel1.Click += new System.EventHandler(this.DirectXPanel1_Clicked);            
            this.view.SourceProcessor1For3DDropdown_SetSelectedIndexChangedHandler(this.VideoSources1For3DDropdown_NewSelection);
            this.view.SourceProcessor2For3DDropdown_SetSelectedIndexChangedHandler(this.VideoSources2For3DDropdown_NewSelection);
            this.view.StartStop3DProcessingButton_SetCheckedChangedHandler(this._3DStartStopButton_OnCheckChange);
            this.view.SecondaryDeviceAllNumerics_SetChangedHandler(this.SecondaryDeviceAnyNumeric_ValueChanged);                        

            this.controllerThread = new Thread(new ThreadStart(this.MyRun));
            this.controllerThread.Start();

            pressedW = false;
            pressedA = false;
            pressedS = false;
            pressedD = false;

            this.view.PreviewKeyDown += Keyboard_PreviewKeyDown;
            this.view.KeyDown += Keyboard_KeyDown;
            this.view.KeyUp += Keyboard_KeyUp;
            this.view.KeyPreview = true;
            
            MouseInterceptor._hookID = MouseInterceptor.SetHook(MouseInterceptor.llmp);
            MouseInterceptor.SetRotateDelegate(this.rotateDXCameraInModel);
        }

        /**
         * Run mostly for measurements.
         */ 
        public void MyRun()
        {
            while (true)
            {
                /**
                 * Measuring VideoSources' FPS.
                 */ 
                if (this.perSecondStopWatch.ElapsedMilliseconds >= MEASURING_INTERVAL)
                {
                    this.perSecondStopWatch.Stop();
                    //string selectedDeviceId = this.model.AvailableVideoDeviceList[this.view.VideoDeviceDropdown.SelectedIndex].MoikerString;
                    string selectedDeviceId = this.model.AvailableVideoDeviceList[this.model.VideoDeviceSelection].MonikerString;

                    /**
                     * Measuring ImageProcessors' processings per seconds.
                     */ 
                    double fps = ((double)this.model.GetNFramesCapturedFromVideoSource(selectedDeviceId) * 1000) / perSecondStopWatch.ElapsedMilliseconds;
                    this.model.ResetNFramesCapturedFromVideoSource(selectedDeviceId);

                    /**
                     * Measuring ImageProcessors' processings per seconds.
                     */ 
                    double pps = ((double)this.model.GetNFramesCapturedFromVideoSource(selectedDeviceId) * 1000) / perSecondStopWatch.ElapsedMilliseconds;
                    this.model.ResetNFramesProcessedInImageProcessor(selectedDeviceId);

                    /**
                     * Measuring ImageProcessors' average searching speed.
                     */ 
                    double averageSearchTime = model.GetProcessorAvgSearchTime(selectedDeviceId);

                    /**
                     * Measuring ImageProcessors' longest searching speed in the latest measuring interval.
                     */ 
                    double longestSearchTime = model.GetProcessorLongestSearchTime(selectedDeviceId);
                                        
                    this.perSecondStopWatch.Restart();

                    string message1 = "&Capturing " + fps.ToString("###0.00") +" frames per second.";
                    string message2 = "&Processing " + pps.ToString("###0.00") + " frames every second.";
                    string message3 = "&Processed in average in " + averageSearchTime.ToString("###0.00") + " milliseconds.";
                    string message4 = "&Longest processing took " + longestSearchTime + " milliseconds.";
                    
                    /*
                    this.view.FPSLabel.Text = message1;
                    this.view.PPSLabel.Text = message2;
                    this.view.ProcessorSpeedLabel.Text = message3;
                    this.view.LongestProcessTimeLabel.Text = message4;
                     */ 
                    
                    SetLabelText(this.view.FPSLabel, message1);
                    SetLabelText(this.view.PPSLabel, message2);
                    SetLabelText(this.view.ProcessorSpeedLabel, message3);
                    SetLabelText(this.view.LongestProcessTimeLabel, message4);                

                    if (fps > 32)
                    {
                        //problem
                    }
                }                                                                                                                        
                
                /**
                 * Preventing busy waiting / processor eating
                 */ 
                try
                {
                    Thread.Sleep(SLEEPTIME);
                }
                catch (ThreadInterruptedException ex)
                {
                    Console.WriteLine("Controller/MyRun(): Thread interrupted.");
                    break;
                }
            }
        }

        /**
         * Not used yet.
         */ 
        private void RefreshButton_Click(object sender, EventArgs e) {
            this.model.RefreshAvailableVideoDeviceList();            
        }

        /**
         * Gets called when the user clicks the Play/Pause button in the Overview tab for the VideoSource.
         * Depending on its current state it starts, resumes, pauses the selected VideoSource.
         */ 
        private void PlayPauseVideoButton_Click(object sender, EventArgs e)
        {
            string deviceId = model.AvailableVideoDeviceList[this.model.VideoDeviceSelection].MonikerString;
            string deviceName = model.AvailableVideoDeviceList[this.model.VideoDeviceSelection].Name;
            if (!this.model.IsVideoSourceInitialized(deviceId)) 
            {                
                //this.model.InitVideoSource(deviceId, deviceName);

                /**
                 * Connecting the VideoSource with the Controller. From now on when the VideoSource captures a new image,
                 * Controller will be notified in the method specified in the second argument.
                 */ 
                this.model.AddResultChangedHandlerToVideoSource(deviceId, VideoSource_NewFrame);
              
                /* Refresh the camera drawing in the DirectXPanel. */
                this.view.DirectXPanel1.CamerasVertexBuffer_OnChange(sender, null);
            }

            if (!model.IsCameraRunning(deviceId))
            {
                this.view.PlayPauseVideoButton.Text = PLAYPAUSE_BUTTON_TEXT_STOP;
                this.model.StartVideoSource(deviceId);                
            }
            else
            {
                view.PlayPauseVideoButton.Text = PLAYPAUSE_BUTTON_TEXT_START;
                this.model.StopImageProcessor(deviceId);
                this.model.StopVideoSource(deviceId);
                this.view.VideoSourcePictureBox.Image = global::Graphene.Properties.Resources.disconnected;                
            }            
        }

        /**
         * Gets called when the user clicks the Play/Pause button in the Overview tab for the ImageProcessor.
         * Depending on its current state it starts, resumes, pauses the selected VideoSource.
         */
        private void PlayPauseProcessorButton_Click(object sender, EventArgs e)
        {
            string deviceId = model.AvailableVideoDeviceList[this.model.VideoDeviceSelection].MonikerString;
            if (!this.model.IsImageProcessorInitialized(deviceId))
            {                                
                //this.model.InitImageProcessor(deviceId);

                /**
                 * Connecting the ImageProcessor to the VideoSource. From now on if deviceId VideoSource has a new image,
                 * deviceId ImageProcessor will be notified (that function as second argument will be called).
                 */
                this.model.AddResultChangedHandlerToVideoSource(deviceId, this.model.ImageProcessors[deviceId].SetSourceToChanged);                    

                /**
                 * Connecting the ImageProcessor with the Controller. From now on if the ImageProcessor states that its
                 * markers have changed, Controller will be notified in the method specified in the second argument.
                 */ 
                this.model.AddResultChangedHandlerToImageProcessor(deviceId, ImageProcessor_MakersChanged);                

                /**
                 * Connecting the ImageProcessor with the Controller. From now on if the ImageProcessor finishes processing
                 * an image, Controller will be notified at that method in the second argument.
                 */ 
                this.model.AddProcessedHandlerToImageProcessor(deviceId, ImageProcessor_Processed);
            }

            if (!model.IsImageProcessorRunning(deviceId))
            {
                view.PlayPauseProcessorButton.Text = PLAYPAUSE_BUTTON_TEXT_STOP;
                this.model.StartImageProcessor(deviceId);                
            }
            else
            {
                view.PlayPauseProcessorButton.Text = PLAYPAUSE_BUTTON_TEXT_START;
                this.model.StopImageProcessor(deviceId);                
                this.view.ImageProcessorPictureBox.Image = global::Graphene.Properties.Resources.disconnected;
            }            
        }

        /**
         * When the user selects a video input device from the view's dropdown list, this method gets called.
         * Draws the necessary camera and ImageProcessor images to the canvas, or if something is not running, clears its canvas.
         */ 
        public void VideoDeviceDropdown_NewSelection(object sender, EventArgs args)
        {
            int index = this.view.VideoDeviceDropdown.SelectedIndex;
            this.model.VideoDeviceSelection = index;
            string deviceId = this.model.AvailableVideoDeviceList[index].MonikerString;
            if (!this.model.IsCameraRunning(deviceId))
            {
                this.view.VideoSourcePictureBox.Image = global::Graphene.Properties.Resources.disconnected;
                this.view.PlayPauseVideoButton.Text = PLAYPAUSE_BUTTON_TEXT_START;
            }
            else
            {
                this.view.PlayPauseVideoButton.Text = PLAYPAUSE_BUTTON_TEXT_STOP;
            }
            if (!this.model.IsImageProcessorRunning(deviceId))
            {
                this.view.ImageProcessorPictureBox.Image = global::Graphene.Properties.Resources.disconnected;
                this.view.PlayPauseProcessorButton.Text = PLAYPAUSE_BUTTON_TEXT_START;
            }
            else
            {
                this.view.PlayPauseProcessorButton.Text = PLAYPAUSE_BUTTON_TEXT_STOP;
            }
        }

        /**
         * Gets called when a new device (ImageProcessor) is selected as source for 3D position computation.
         * It sets the selection attribute in the model and updates the remaining possibilities for the
         * remaining similar dropdowns.
         */ 
        private void VideoSources1For3DDropdown_NewSelection(object sender, EventArgs args)
        {
            this.model.SourceProcessor1For3DSelection = this.view.SourceProcessor1For3DDropdownSelectedItem();            
        }

        private void VideoSources2For3DDropdown_NewSelection(object sender, EventArgs args)
        {
            this.model.SourceProcessor2For3DSelection = this.view.SourceProcessor2For3DDropdownSelectedItem();
        }

        public void MouselookCheckbox_OnCheckChange(object sender, EventArgs args)
        {
            if (this.view.MouselookCheckbox.CheckState.Equals(CheckState.Checked))
            {
                this.view.MouselookCheckbox.Text = MainForm.MOUSELOOK_CHECKBOX_CHECKED_TEXT;
                this.model.MouseLookAndMovementEnabled = true;
            }
            else if (this.view.MouselookCheckbox.CheckState.Equals(CheckState.Unchecked))
            {
                this.view.MouselookCheckbox.Text = MainForm.MOUSELOOK_CHECKBOX_UNCHECKED_TEXT;
                this.model.MouseLookAndMovementEnabled = false;
            }
        }

        public void DirectXPanel1_Clicked(object sender, EventArgs args)
        {
            if (this.view.MouselookCheckbox.CheckState.Equals(CheckState.Unchecked))
            {
                this.view.MouselookCheckbox.Checked = true;
            }
        }

        private void _3DStartStopButton_OnCheckChange(object sender, EventArgs args)
        {
            if (this.view.StartStop3DProcessingButton.CheckState.Equals(CheckState.Checked))
            {
                this.model.Init3DProcessor();
                this.model.ThreeDProcessor.AddResultChangedHandler(this.ThreeDProcessor_ResultChanged);
                this.model.Start3DProcessor();
            }
            else if (this.view.StartStop3DProcessingButton.CheckState.Equals(CheckState.Unchecked))
            {
                this.model.Pause3DProcessor();
            }
        }

        /* Gets called whenever the angle or the position of the secondary camera is changed at
         * the numerics. */
        private void SecondaryDeviceAnyNumeric_ValueChanged(object sender, EventArgs args)
        {
            float[] data = this.view.SecondaryDevicePositionAndAngle;
            PointReal3D position = new PointReal3D(data[0], data[1], data[2]);            
            PointReal2D angle = new PointReal2D(data[3], data[4]);            
            this.model.SetCameraPositionAndAngle(model.SourceProcessor2For3DSelection.MyId, position, angle);
            this.view.DirectXPanel1.CamerasVertexBuffer_OnChange(sender, null);
        }

        /**
         * Gets called whenever a new frame is captured in a VideoSource.
         * Draws the captured image on the view. A fast resizing is also included.
         */
        private void VideoSource_NewFrame(Object sender, Object whatChanged)
        {            
            VideoSource senderVideoSource = (VideoSource)sender;
            if ((object)senderVideoSource == null)
            {
                return;
            }
            string deviceId = senderVideoSource.MyId;
            
            if (deviceId.Equals(this.model.AvailableVideoDeviceList[this.model.VideoDeviceSelection].MonikerString))
            {
                byte[] byteImage = this.model.GetByteImageFromVideoSource(deviceId);                                
                Bitmap bitmap = this.model.GetBitmapFromVideoSource(deviceId);
                if (bitmap != null && byteImage != null)
                {
                    byte[] resizedByteImage = AbstractProcessor.Resize(byteImage, bitmap.Width, bitmap.Height, MainForm.VIEWFINDER_WIDTH, MainForm.VIEWFINDER_HEIGHT);
                    Bitmap resizedBitmap = AbstractProcessor.ByteImageToBitmap_Unsafe(resizedByteImage, 320, 240);
                    this.view.VideoSourcePictureBox.Image = resizedBitmap;
                }
                else
                {
                    this.view.VideoSourcePictureBox.Image = global::Graphene.Properties.Resources.o;
                }                
            }                                              
        }

        /**
         * Draw the image, on which the ImageProcessor was computing. Also show the found markers, spats,
         * additional debugging or scientific :DD data.
         */ 
        private void ImageProcessor_Processed(object sender)
        {
            MultiColorImageProcessor senderImageProcessor = (MultiColorImageProcessor)sender;
            if ((object)senderImageProcessor == null)
            {
                return;
            }

            string deviceId = senderImageProcessor.MyId;
            if (deviceId.Equals(this.model.AvailableVideoDeviceList[this.model.VideoDeviceSelection].MonikerString))
            {
                byte[] byteImage = this.model.GetProcessorByteImage(deviceId);                
                Dictionary<string, PointInt2D> firstPoints = this.model.GetFirstPixelsFoundFromImageProcessor(deviceId);                                
                Dictionary<string, Spat> spats = this.model.GetSpatsFromImageProcessor(deviceId);
                Dictionary<string, MarkerInImage> markers = this.model.GetMarkersFromImageProcessor(deviceId);

                byte[] modifiedByteImage = new byte[senderImageProcessor.CopiedImageSize.X * senderImageProcessor.CopiedImageSize.Y * 3];
                byteImage.CopyTo(modifiedByteImage, 0);
                
                /**
                 * Draw spats.
                 */
                /*
                for (int i = 0; i < markers.Count; i++)
                {
                    if (markers[i].Exists)
                    {
                        modifiedByteImage = ImageProcessor.SpatOverByteImage(spats[i], modifiedByteImage, senderImageProcessor.ImageSize.X, senderImageProcessor.ImageSize.Y, Color.LightGray, 0.9);                        
                    }
                }    
                */ 

                /**
                 * Resize.
                 */ 
                modifiedByteImage = AbstractProcessor.ResizeAndFadeToColor(modifiedByteImage, senderImageProcessor.CopiedImageSize.X, senderImageProcessor.CopiedImageSize.Y, MainForm.VIEWFINDER_WIDTH, MainForm.VIEWFINDER_HEIGHT, Color.FromArgb(0, 0, 0), 0.4);                
                
                /**
                 * Draw first points and markers (Xs).
                 */ 
                foreach (MarkerInImage localMarker in markers.Values)
                {                                
                    if (localMarker.Exists)
                    {
                        //modifiedByteImage = ImageProcessor.DrawX(modifiedByteImage, MainForm.VIEWFINDER_WIDTH, MainForm.VIEWFINDER_HEIGHT, new Point2D((int)(firstPoints[i].X / ((double)senderImageProcessor.ImageSize.X / MainForm.VIEWFINDER_WIDTH)), (int)(firstPoints[i].Y / ((double)senderImageProcessor.ImageSize.Y / MainForm.VIEWFINDER_HEIGHT))), Color.Gray); outdated coordinates
                        modifiedByteImage = AbstractProcessor.DrawX(
                            modifiedByteImage,
                            MainForm.VIEWFINDER_WIDTH,
                            MainForm.VIEWFINDER_HEIGHT,
                            new PointInt2D(
                                (int)(localMarker.Position.X * MainForm.VIEWFINDER_WIDTH / 2 + MainForm.VIEWFINDER_WIDTH / 2),
                                (int)(localMarker.Position.Y * MainForm.VIEWFINDER_HEIGHT / 2 + MainForm.VIEWFINDER_HEIGHT / 2)
                            ),
                            localMarker.RepresentingColor
                        );
                    }
                }
                Bitmap shownBitmap = AbstractProcessor.ByteImageToBitmap_Unsafe(modifiedByteImage, MainForm.VIEWFINDER_WIDTH, MainForm.VIEWFINDER_HEIGHT);                
                this.view.ImageProcessorPictureBox.Image = shownBitmap;
            }
        }

        private void ThreeDProcessor_ResultChanged(object sender, object whatChanged)
        {                            
            this.view.DirectXPanel1.MarkersVertexBuffer_OnCreate(sender, EventArgs.Empty);
        }

        /**
         * What to do with the markers. High level application business continues from here.
         * Stereo starts from here. Applications of 2D operations should also start from here.
         */ 
        private void ImageProcessor_MakersChanged(Object sender, Object whatChanged)
        {
            /* Show the markers' beams on the DirectXPanel. */
            this.view.DirectXPanel1.BeamsVertexBuffer_OnChange(sender, EventArgs.Empty);
        }

        private void Keyboard_PreviewKeyDown(object sender, PreviewKeyDownEventArgs args)
        {            
        }

        private void Keyboard_KeyDown(object sender, KeyEventArgs args)
        {          
            if (args.KeyData.Equals(Keys.W) && !this.pressedW)
            {
                this.pressedW = true;
                args.Handled = true;
                this.model.DXCameraIsMovingForward = true;                               
            }
            if (args.KeyData.Equals(Keys.A) && !this.pressedA)
            {
                this.pressedA = true;
                args.Handled = true;
                this.model.DXCameraIsStrafingLeft = true;
            }
            if (args.KeyData.Equals(Keys.S) && !this.pressedS)
            {
                this.pressedS = true;
                args.Handled = true;
                this.model.DXCameraIsMovingBackward = true;
            }
            if (args.KeyData.Equals(Keys.D) && !this.pressedD)
            {
                this.pressedD = true;
                args.Handled = true;
                this.model.DXCameraIsStrafingRight = true;
            }
        }

        private void Keyboard_KeyUp(object sender, KeyEventArgs args)
        {
            if (args.KeyData.Equals(Keys.Escape))
            {
                if (this.view.MouselookCheckbox.CheckState.Equals(CheckState.Checked))
                {
                    this.view.MouselookCheckbox.Checked = false;
                }
            }
            if (args.KeyData.Equals(Keys.W))
            {
                this.pressedW = false;
                args.Handled = true;
                this.model.DXCameraIsMovingForward = false;
            }
            if (args.KeyData.Equals(Keys.A))
            {
                this.pressedA = false;
                args.Handled = true;
                this.model.DXCameraIsStrafingLeft = false;
            }
            if (args.KeyData.Equals(Keys.S))
            {
                this.pressedS = false;
                args.Handled = true;
                this.model.DXCameraIsMovingBackward = false;
            }
            if (args.KeyData.Equals(Keys.D))
            {
                this.pressedD = false;
                args.Handled = true;
                this.model.DXCameraIsStrafingRight = false;
            }
            if (args.KeyData.Equals(Keys.P))
            {
                DirectXPanel.LOFASZ = true;
            }
        }

        public void rotateDXCameraInModel(float byY, float byX)
        {            
            model.RotateDXCamera(byY, byX);
        }


        /**
         * What happens when the user clicks the x in the upper right corner. Or presses alt+F4. Or whatever closing action.
         */ 
        private void MainFormClosed(object sender, FormClosedEventArgs e)
        {
            this.controllerThread.Abort();
            this.model.CloseAllImageProcessors();
            this.model.CloseAllVideoSources();

            MouseInterceptor.UnhookWindowsHookEx(MouseInterceptor._hookID);            
        }

        private void ExitToolStripMenuItem_Click(object sender, EventArgs e)
        {
            Application.Exit();
        }



        private delegate void SetTextCallback(Label label, string text);
        private void SetLabelText(Label label, string text)
        {
            if (label.InvokeRequired)                
            {
                SetTextCallback d = new SetTextCallback(SetLabelText);
                if (!this.view.IsDisposed && !this.view.Disposing)
                {
                    this.view.Invoke(d, new object[] {label, text});
                }
            }
            else
            {
                label.Text = text;
            }
        }


        private Model model;
        private MainForm view;
        private MarkerCalibrateController markerCalibrateController;
        private StereoProcessingController _3dProcessingController;


        private Stopwatch perSecondStopWatch;

        /**
         * The upper run() thing. For measuring.
         */
        private Thread controllerThread;

        private bool pressedW;
        private bool pressedS;
        private bool pressedA;
        private bool pressedD;
    }
}
